﻿/*
 * this code is based on harold aptroot article with some 
 * modification you can check out the original code from 
 * code project website 
 * link http://www.codeproject.com/Articles/27992/NAT-Traversal-with-UPnP-in-C
 */

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Xml;
using System.Windows.Forms;
using System.Xml.Linq;

namespace NATP2P.UPNP
{
    /// <summary>
    /// upnp protocol implementation for manage port forwarding on gatway
    /// </summary>
    public class NATUPNP
    {
        TimeSpan _timeout = new TimeSpan(0, 0, 0, 3);
        /// <summary>
        /// set timeout for waiting response
        /// </summary>
        public TimeSpan TimeOut
        {
            get { return _timeout; }
            set { _timeout = value; }
        }
        static string _descUrl, _serviceUrl, _eventUrl;
        /// <summary>
        /// check if upnp is enabled
        /// </summary>
        /// <returns>true if enabled otherways false</returns>
        public bool Discovery()
        {
            Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

            String req = "M-SEARCH * HTTP/1.1\r\n"
                          + "HOST: 239.255.255.250:1900\r\n"
                          + "MAN: \"ssdp:discover\"\r\n"
                          + "MX: 5\r\n"
                          + "ST:upnp:rootdevice\r\n"
                          + "\r\n";
            byte[] data = Encoding.ASCII.GetBytes(req);
            IPEndPoint ipe = new IPEndPoint(IPAddress.Parse("239.255.255.250"), 1900);
            byte[] buffer = new byte[4096];

            DateTime start = DateTime.Now;

            do
            {
                s.SendTo(data, ipe);
              //  s.SendTo(data, ipe);
              //  s.SendTo(data, ipe);

                int length = 0;
                do
                {
                    length = s.Receive(buffer);

                    string resp = Encoding.ASCII.GetString(buffer, 0, length);
                    if (resp.Contains("upnp:rootdevice"))
                    {
                        resp = resp.Substring(resp.ToLower().IndexOf("location:") + 9);

                        resp = resp.Substring(0, resp.IndexOf("\r")).Trim();

                        if (!string.IsNullOrEmpty(_serviceUrl = GetServiceUrl(resp)))
                        {
                          
                            _descUrl = resp;
                            s.Dispose();
                            return true;
                        }
                    }
                } while (length > 0);
            } while (start.Subtract(DateTime.Now) < _timeout);
            s.Dispose();
            return false;
        }

        private string GetServiceUrl(string resp)
        {

            try
            {

                XmlDocument desc = new XmlDocument();
                desc.Load(resp);     
                XmlNamespaceManager nsMgr = new XmlNamespaceManager(desc.NameTable);
                nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0");
                XmlNode typen = desc.SelectSingleNode("//tns:device/tns:deviceType/text()", nsMgr);
                if (!typen.Value.Contains("InternetGatewayDevice"))
                    return null;
                XmlNode node = desc.SelectSingleNode("//tns:service[tns:serviceType=\"urn:schemas-upnp-org:service:WANIPConnection:1\"]/tns:controlURL/text()", nsMgr);
                if (node == null)
                    return null;
                XmlNode eventnode = desc.SelectSingleNode("//tns:service[tns:serviceType=\"urn:schemas-upnp-org:service:WANIPConnection:1\"]/tns:eventSubURL/text()", nsMgr);
                _eventUrl = CombineUrls(resp, eventnode.Value);
                Console.WriteLine(_eventUrl);
                return CombineUrls(resp, node.Value);

            }
            catch { return null; }

        }

        private static string CombineUrls(string resp, string p)
        {
            int n = resp.IndexOf("://");
            n = resp.IndexOf('/', n + 3);

            return resp.Substring(0, n) + p;
        }
        /// <summary>
        /// add forward port to gatway
        /// </summary>
        /// <param name="internalPort">private port <see cref="T:System.Int32" /></param>
        /// <param name="internelhost">private ip address <see cref="T:System.String" /></param>
        /// <param name="externalPort">public port <see cref="T:System.Int32" /></param>
        /// <param name="protocol">protocol type TCP or UDP <see cref="T:System.Net.Sockets.ProtocolType" /></param>
        /// <param name="description"> Friendly name or description of service <see cref="T:System.String" /></param>
        /// <param name="enabled"> status <see cref="T:System.Boolean" /></param>
        public void PortForwarding(int internalPort, string internelhost, int externalPort, ProtocolType protocol, string description, bool enabled)
        {
            if (string.IsNullOrEmpty(_serviceUrl))
                throw new Exception("No UPnP service available or Discover() has not been called");
            XmlDocument xdoc = SOAPRequest(_serviceUrl, "<u:AddPortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">" +
            "<NewRemoteHost></NewRemoteHost><NewExternalPort>" + externalPort.ToString() + "</NewExternalPort><NewProtocol>" + protocol.ToString().ToUpper() + "</NewProtocol>" +
            "<NewInternalPort>" + internalPort.ToString() + "</NewInternalPort><NewInternalClient>" + internelhost +
            "</NewInternalClient><NewEnabled>" + Convert.ToInt16(enabled).ToString() + "</NewEnabled><NewPortMappingDescription>" + description +
            "</NewPortMappingDescription><NewLeaseDuration>0</NewLeaseDuration></u:AddPortMapping>", "AddPortMapping");
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="externalPort"></param>
        /// <param name="protocol"></param>
        public void RemovePortForwarding(int externalPort, ProtocolType protocol)
        {
            if (string.IsNullOrEmpty(_serviceUrl))
                throw new Exception("No UPnP service available or Discover() has not been called");
            XmlDocument xdoc = this.SOAPRequest(_serviceUrl,
            "<u:DeletePortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">" +
            "<NewRemoteHost>" +
            "</NewRemoteHost>" +
            "<NewExternalPort>" + externalPort + "</NewExternalPort>" +
            "<NewProtocol>" + protocol.ToString().ToUpper() + "</NewProtocol>" +
            "</u:DeletePortMapping>", "DeletePortMapping");
        }

        /// <summary>
        /// get the public ip address
        /// </summary>
        /// <returns>return ip address</returns>
        public IPAddress GetExternalIP()
        {
            if (string.IsNullOrEmpty(_serviceUrl))
                throw new Exception("No UPnP service available or Discover() has not been called");
            XmlDocument xdoc = this.SOAPRequest(_serviceUrl, "<u:GetExternalIPAddress xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">" +
            "</u:GetExternalIPAddress>", "GetExternalIPAddress");
            XmlNamespaceManager nsMgr = new XmlNamespaceManager(xdoc.NameTable);
            nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0");
            string IP = xdoc.SelectSingleNode("//NewExternalIPAddress/text()", nsMgr).Value;
            return IPAddress.Parse(IP);
        }

        private XmlDocument SOAPRequest(string url, string soap, string function)
        {
            string req = "<?xml version=\"1.0\"?>" +
            "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" +
            "<s:Body>" +
            soap +
            "</s:Body>" +
            "</s:Envelope>";
            WebRequest r = HttpWebRequest.Create(url);
            r.Method = "POST";
            byte[] b = Encoding.UTF8.GetBytes(req);
            r.Headers.Add("SOAPACTION", "\"urn:schemas-upnp-org:service:WANIPConnection:1#" + function + "\"");
            r.ContentType = "text/xml; charset=\"utf-8\"";
            r.ContentLength = b.Length;
            r.GetRequestStream().Write(b, 0, b.Length);
            XmlDocument resp = new XmlDocument();
            WebResponse wres = r.GetResponse();
            Stream ress = wres.GetResponseStream();
            resp.Load(ress);
            return resp;
        }

        /// <summary>
        /// get port entry associte information
        /// </summary>
        /// <returns>list of PortMappingEntry</returns>
        public List<PortMappingEntry> GetGenericPortMappingEntry()
        {
            int NewPortMappingIndex = 0;
            List<PortMappingEntry> list = new List<PortMappingEntry>();
            while (true)
            {
               try
                {

                    PortMappingEntry portmappingentry = new PortMappingEntry();
                    XmlDocument xdoc = SOAPRequest(_serviceUrl, "<u:GetGenericPortMappingEntry xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">" +
                                                                "<NewPortMappingIndex>" + NewPortMappingIndex + "</NewPortMappingIndex>" +
                                                                "</u:GetGenericPortMappingEntry>", "GetGenericPortMappingEntry");
                    XmlNamespaceManager nsMgr = new XmlNamespaceManager(xdoc.NameTable);
                    nsMgr.AddNamespace("tns", "urn:schemas-upnp-org:device-1-0");
                    portmappingentry.NewExternalPort = xdoc.SelectSingleNode("//NewExternalPort/text()", nsMgr).Value;
                    portmappingentry.NewProtocol = xdoc.SelectSingleNode("//NewProtocol/text()", nsMgr).Value;
                    portmappingentry.NewInternalPort = xdoc.SelectSingleNode("//NewInternalPort/text()", nsMgr).Value;
                    portmappingentry.NewInternalClient = xdoc.SelectSingleNode("//NewInternalClient/text()", nsMgr).Value;
                    portmappingentry.NewEnabled = xdoc.SelectSingleNode("//NewEnabled/text()", nsMgr).Value;
                    portmappingentry.NewPortMappingDescription = xdoc.SelectSingleNode("//NewPortMappingDescription/text()", nsMgr).Value;
                    list.Add(portmappingentry);
                    NewPortMappingIndex++;
                    
                }
                catch (Exception )
                {
                  // MessageBox.Show(NewPortMappingIndex.ToString());
                    return list;
                    // throw;
                }


            }


        }
    }
}
